#pragma once

#include<iostream>
#include<unistd.h>
#include<cstdint>
#include<string>
#include<queue>
#include<pthread.h>

const static uint32_t gcap = 5;

template<typename T>
class BlockQueue
{
private:
    bool IsFull()
    {
        if(_bq.size() >= _cap) return true;
        return false;
    }
    bool IsEmpty()
    {
        if(_bq.empty()) return true;
        return false;
    }
public:
    BlockQueue(uint32_t cap = gcap):_cap(cap),_c_wait_num(0),_p_wait_num(0)
    {
        //对锁、条件变量进行初始化
        pthread_mutex_init(&_lock , nullptr);
        pthread_cond_init(&_c_cond , nullptr);
        pthread_cond_init(&_p_cond , nullptr);
    }
    void Enqueue(const T& in)//引用传参
    {
        //向阻塞队列中放入数据
        //因为会生产者、消费者均会访问同一个阻塞队列，所以此处需要加锁处理实现互斥
        //先加锁
        pthread_mutex_lock(&_lock);
        //判断阻塞队列中的数据是否满了，满了，生产者就不生产了，到消费者的条件变量下等待
        while(IsFull())
        {
            _c_wait_num++;
            pthread_cond_wait(&_c_cond , &_lock);//去消费者条件变量下的等待队列之前会解锁，pthread_cond_wait自动完成的
            //所以第二个参数需要填锁
            _c_wait_num--;
        }
        //pthread_cond_wait 函数也有可能会调用失败，且这个函数存在伪唤醒的情况；即在条件不满足的情况下唤醒了一个线程
        //这个线程会从pthread_cond_wait 的位置往后执行代码，从而可能导致在不满足的生产、消费条件的情况下
        //去生产与消费，例如：阻塞队列满了，但是生产者仍然向队列中做写入；想要解决这样的问题，只需要让被唤醒的线程再执行
        //进入该等待逻辑的条件，简单来说就是将if 改为 while

        //走到了这里就是不满的情况，此时就可以向阻塞队列中放数据
        _bq.push(in);//生产者完成生产
        //生产者生产完数据就可以通知消费者了，即唤醒自己条件变量下等待的线程
        if(_p_wait_num) pthread_cond_signal(&_p_cond);//这个函数放在临界区中还是临界区外都可以
        //因为一个线程被唤醒是从之前的pthread_cond_wait 的语句继续往下执行，在此之前需要竞争锁，因为pthread_cond_wait
        //在临界区之中，如果竞争锁失败，这个线程就会到锁的等待队列中等待；
        pthread_mutex_unlock(&_lock);
    }
    void Pop(T* out)//传址传参
    {
        //加锁
        pthread_mutex_lock(&_lock);
        //判断阻塞队列是否为空，为空就不从阻塞队列中获取数据，且到生产者所对应条件变量的等待队列中进行等待
        while(IsEmpty())
        {
            _p_wait_num++;
            pthread_cond_wait(&_p_cond , &_lock);//消费者到生产者条件变量的等待队列中首先需要解锁
            _p_wait_num--;
        }
        //走到此处就说明阻塞队列中有数据
        *out = _bq.front();
        _bq.pop();
        //消费者消费了一个数据就可以通知生产者了，即唤醒自己条件变量下所等待的线程
        if(_c_wait_num) pthread_cond_signal(&_c_cond);
        //解锁
        pthread_mutex_unlock(&_lock);
    }
    ~BlockQueue()
    {
        //释放锁、条件变量
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_c_cond);
        pthread_cond_destroy(&_p_cond);
    }
private:
    //阻塞队列底层使用队列,不指定数据类型所以此处需要使用模板
    std::queue<T> _bq;
    uint32_t _cap;//因为容器没有上限，所以我们需要定义一个变量来标识上限
    //一把锁就行了，因为访问的是一个临界资源
    pthread_mutex_t _lock;
    //到彼此的条件变量下等待，所以此处需要创建两个条件变量
    pthread_cond_t _c_cond;
    pthread_cond_t _p_cond;

    //如果条件变量中等待队列中没有线程，如果唤醒默认处理动作为忽略，其实也还有一种处理方法
    //多定义两个变量来记录条件变量等待队列中线程的个数，如果为0则不唤醒
    int _c_wait_num;
    int _p_wait_num;
};